#pragma once

#include <cstdint>
#include <type_traits>
#include <vex/iterable/range_base.h>

namespace vex { namespace query { namespace adapter { namespace detail {
    enum class where_adapter_state : std::int32_t {
        not_initialized = 0,
        initialized,
        started,
        ended
    };

    template< class TAdaptee, class TPredicate >
    class where_adapter_body_base {
    protected:
        typedef typename iterable::range_traits<TAdaptee> adaptee_traits;

        typedef typename adaptee_traits::range_type adaptee_type;

        typedef typename std::decay<TPredicate>::type predicate_type;

        adaptee_type m_adaptee;
        predicate_type m_predicate;

        bool move_front_impl()
        {
            while (m_adaptee.move_front()) {
                if (m_predicate(m_adaptee.front())) {
                    return true;
                }
            }
            return false;
        }

        bool move_back_impl()
        {
            while (m_adaptee.move_back()) {
                if (m_predicate(m_adaptee.back())) {
                    return true;
                }
            }
            return false;
        }

        where_adapter_body_base(
            TAdaptee p_adaptee, 
            TPredicate p_predicate)
            : m_adaptee(p_adaptee)
            , m_predicate(p_predicate) {}

        where_adapter_body_base(where_adapter_body_base&& p_other)
            : m_adaptee(std::move(p_other.m_adaptee))
            , m_predicate(std::move(p_other.m_predicate)) {}
    public:
        typename adaptee_traits::reference front() const
        {
            return this->m_adaptee.front();
        }

        typename adaptee_traits::reference back() const
        {
            return this->m_adaptee.back();
        }
    };

    struct less_then_bidirectional;
    struct geq_then_bidirectional;

    /// Implementation of where_adapter_body for less than bidirectional ranges
    /// No need to manage interal state, the call to move_front is sufficent
    template< class TAdaptee, class TPredicate, class TCategory = less_then_bidirectional > 
    class where_adapter_body : public where_adapter_body_base<TAdaptee, TPredicate> {
        typedef where_adapter_body_base<TAdaptee, TPredicate> base_type;

        using typename base_type::adaptee_traits;
    public:
        // in case the category is more than one_pass... 
        where_adapter_body save() const
        {
            return where_adapter_body(this->m_adaptee.save(), this->m_predicate);
        }

        bool move_front()
        {
            return this->move_front_impl();
        }

        where_adapter_body(TAdaptee p_adaptee, TPredicate p_predicate)
            : base_type(p_adaptee, p_predicate) {}

        where_adapter_body(where_adapter_body&& p_other)
            : base_type(std::move(p_other)) {}
    };

    /// Specialization for ranges of greater or equal to bidirectional category
    /// Need to initialized during construction such that front and back are
    /// properly adjusted to first (last) elements which satisify the predicate
    template< class TAdaptee, class TPredicate > 
    class where_adapter_body<TAdaptee, TPredicate, geq_then_bidirectional> 
        : public where_adapter_body_base<TAdaptee, TPredicate> 
    {
        typedef where_adapter_body_base<TAdaptee, TPredicate> base_type;

        using typename base_type::adaptee_traits;

        where_adapter_state m_state;

        void initialize(iterable::one_pass const &)
        {
            m_state = this->move_front_impl() ? 
                where_adapter_state::initialized : where_adapter_state::ended;
        }

        void initialize(iterable::bidirectional const &)
        {
            initialize(iterable::one_pass()); 

            if (where_adapter_state::initialized == m_state) {
                bool t_can_move = false;
                do {
                    t_can_move = this->m_predicate(this->m_adaptee.back());
                } while (!t_can_move && this->m_adaptee.move_back());

                m_state = t_can_move ?
                    where_adapter_state::initialized : where_adapter_state::ended;
            }
        }
    public:
        where_adapter_body save() const
        {
            return where_adapter_body(m_adaptee.save(), m_predicate, m_state);
        }

        bool move_front()
        {
            switch (m_state) {
            case where_adapter_state::initialized:
                m_state = where_adapter_state::started;
                return true;
            case where_adapter_state::started:
                m_state = this->move_front_impl() ?
                    where_adapter_state::started : where_adapter_state::ended;
                return where_adapter_state::started == m_state;
            default:
                return false;
            }

        }

        bool move_back()
        {
            switch (m_state) {
            case where_adapter_state::initialized:
                m_state = where_adapter_state::started;
                return true;
            case where_adapter_state::started:
                m_state = this->move_back_impl() ?
                    where_adapter_state::started : where_adapter_state::ended;
                return where_adapter_state::started == m_state;
            default:
                return false;
            }
        }

        where_adapter_body(
            TAdaptee p_adaptee, 
            TPredicate p_predicate, 
            where_adapter_state p_state = where_adapter_state::not_initialized)
            : base_type(p_adaptee, p_predicate)
            , m_state(p_state) 
        {
            if (m_state == where_adapter_state::not_initialized) {
                initialize(adaptee_traits::category());
            }
        }

        where_adapter_body(where_adapter_body&& p_other)
            : base_type(std::move(p_other))
            , m_state(p_other.m_state) {}
    };
}}}}